﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace Concat
{
    class MultiMethod
    {
        public string name;
        public List<Method> methods;

        public MultiMethod(string name)
        {
            this.name = name;
            this.methods = new List<Method>();
        }
        private MultiMethod(string name, List<Method> methods)
        {
            this.name = name;
            this.methods = methods;
        }

        public void addMethod(Method method)
        {
            for (int i = 0; i < methods.Count; i++)
            {
                if (methods[i].sameArgTypes(method))
                {
                    methods[i] = method;
                    return;
                }
            }
            methods.Add(method);
        }

        public Method getMethod(CallStack callStack, params ConcatType[] types)
        {
            // find the method that
            // 1) accepts the types, and
            // 2) is more specific than every other accepting method
            List<Method> accepting = new List<Method>();
            foreach (Method method in methods)
                if (method.accepts(types))
                    accepting.Add(method);
            if (accepting.Count == 0)
                throw new NoMethodMatchException(callStack, this, types);
            Method best = accepting[0];
            int bestIndex = 0;
            for (int i = 1; i < accepting.Count; i++)
                if (accepting[i].moreSpecificThan(best))
                {
                    best = accepting[i];
                    bestIndex = i;
                }
            List<Method> options = new List<Method>() { best };
            for (int i = 0; i < accepting.Count; i++)
                if (i != bestIndex && !best.moreSpecificThan(accepting[i]))
                    options.Add(accepting[i]);
            if (options.Count != 1)
                throw new AmbiguousMethodMatchException(callStack, this, types, options.ToArray());
            return options[0];
        }

        public MultiMethod substType(ConcatType oldType, ConcatType newType)
        {
            return new MultiMethod(this.name, new List<Method>(this.methods.map(method => method.substType(oldType, newType))));
        }
    }
    class Method
    {
        public string[] formals;
        public ConcatType[] types;
        public string[] paramNames;
        public string name;
        public Location bodyLoc;
        public Term[] def;

        public Method(ConcatType[] types, string[] formals, string[] paramNames, string name,
            Location bodyLoc, Term[] def)
        {
            this.name = name;
            this.formals = formals;
            this.types = types;
            this.paramNames = paramNames;
            this.bodyLoc = bodyLoc;
            this.def = def;

            for (int i = 0; i < paramNames.Length; i++)
                if (paramNames[i] == "_")
                    this.paramNames[i] = null;
        }

        public static bool sameTypes(Method a, Method b)
        {
            return a.name == b.name && a.sameArgTypes(b);
        }

        public Method substType(ConcatType oldType, ConcatType newType)
        {
            ConcatType[] newTypes = new ConcatType[types.Length];
            for (int i = 0; i < types.Length; i++)
                newTypes[i] = (types[i] == oldType) ? newType : types[i];
            return new Method(newTypes, formals, paramNames, name, bodyLoc, def);
        }

        public bool accepts(ConcatType[] argTypes)
        {
            if (types.Length != argTypes.Length)
                return false;
            for (int i = 0; i < types.Length; i++)
                if (!types[i].covers(argTypes[i]))
                    return false;
            return true;
        }
        // expects this.types.Length == other.types.Length
        public bool moreSpecificThan(Method other)
        {
            for (int i = 0; i < types.Length; i++)
                if (this.types[i].covers(other.types[i]))
                    return false;
            return true;
        }

        public bool sameArgTypes(Method other)
        {
            if (types.Length != other.types.Length)
                return false;
            for (int i = 0; i < types.Length; i++)
                if (types[i] != other.types[i])
                    return false;
            return true;
        }
    }

    class NoMethodMatchException : RuntimeException
    {
        public MultiMethod multimethod;
        public ConcatType[] argTypes;

        public NoMethodMatchException(CallStack stack, MultiMethod multimethod, ConcatType[] argTypes)
            : base("Multimethod '" + multimethod.name + "' could not match argument types "
                   + argTypes.map(type => "'" + type.name + "'").interleave(", ").concat() + " at {loc}.", 
                   stack)
        {
            this.multimethod = multimethod;
            this.argTypes = argTypes;
        }
    }
    class AmbiguousMethodMatchException : RuntimeException
    {
        public MultiMethod multimethod;
        public ConcatType[] argTypes;
        public Method[] options;

        public AmbiguousMethodMatchException(CallStack stack, MultiMethod multimethod,
            ConcatType[] argTypes, Method[] options)
            : base("Ambiguous match for multimethod '" + multimethod.name + "' for argument types "
                   + argTypes.map(type => "'" + type.name + "'").interleave(", ").concat()
                   + " at {loc}.", stack)
        {
            this.multimethod = multimethod;
            this.argTypes = argTypes;
            this.options = options;
        }
    }
}
